#!/usr/bin/python

# Copyright 2010 Mikko Rantanen
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import fuse
from fuse import Direntry, FuseFileInfo, Fuse
from time import time, sleep as time_sleep_method

import stat
import sys
import os
import errno
import pprint
import traceback

import logging

import service.rest as rest
import model.vault as vault
import bus.commbus as bus
import client.cmds as cmds

LOG_FILENAME = 'fuse.log'
logging.basicConfig(filename=LOG_FILENAME,level=logging.DEBUG)

openfiles = {}

# Wrapper methods for encoding and decoding the 8-bit strings
# that FUSE uses to unicode strings that MFilesFS uses internally.
def fsencode(s):
    return s.encode('utf-8')

def fsdecode(s):
    return s.decode('utf-8')

class FsFile():
    def __init__(self, fs, path, flags, mode = None):
        #FuseFileInfo.__init__(self)
        self.fs = fs

        path = fsdecode(path)
        self.item = self.fs._get_fsentry(path)
        self.item.open(flags)
        logging.info("File class constructed")

        # Remove these two attributes if you hate the next maintainer.
        self.keep_cache = False
        self.direct_io = False

    def read(self, size, offset):
        logging.debug("Called: read")
        logging.debug("%s, %s" % (size, offset))
        r = self.item.read(size, offset)
        logging.debug("Read: %s" % r)
        return r

    def write(self, buf, offset):
        logging.debug("Called: write")
        return self.item.write(buf, offset)

    def fsync(self, *a):
        logging.debug("Called: fsync")
        logging.error("Unimplemented fsync, parameters: %s" % len(a))
        return -errno.ENOSYS

    def release(self, flags):
        logging.debug("Called: release")
        return self.item.release(flags)

    def flush(self):
        logging.debug("Called: flush")
        return 0

    def fgetattr(self):
        logging.debug("Called: fgetattr")
        return self.item

    def ftruncate(self, *a):
        logging.debug("Called: ftruncate")
        logging.error("Unimplemented ftruncate, parameters: %s" % len(a))
        return -errno.ENOSYS

    def lock(self, *a, **kw):
        logging.error("Unimplemented lock, parameters: %s" % len(a))
        return -errno.ENOSYS


class FsDir():
    def __init__(self, fs, path):
        logging.info("Constructing dir class: %s" % path)
        self.fs = fs
        path = fsdecode(path)
        self.item = self.fs._get_fsentry(path)
        logging.info("Dir class constructed")

    def readdir(self, offset):
        logging.debug('readdir: %s' % "test")

        # Always return '.' and '..'
        dirents = [ '.', '..' ]

        if self.item is self.fs.vault:
            dirents.append('.mfilesfs')

        contents = self.item.get_contents()
        logging.debug('%s items received from server' % len(contents))

        # FUSE wants 8-bit strings so use some sort of encoding.
        subdirnames = map(lambda c: fsencode(c.get_name()), contents)

        # TODO: Figure out what to do with empty values.
        # There were empty values at some point which caused issues.
        subdirnames = filter(lambda n: n != '', subdirnames)

        logging.debug('directory contains %s items' % len(subdirnames))

        dirents.extend(subdirnames)

        logging.debug('listing %s items' % len(dirents))
        logging.debug(str(dirents))
        for r in dirents:
            yield fuse.Direntry(r)

    def fsyncdir(self, buf, offset):
        logging.error("Calling unimplemented method: fsyncdir")
        return -errno.ENOSYS

    def releasedir(self):
        logging.error("Calling unimplemented method: releasedir")
        return -errno.ENOSYS

class MFFS(Fuse):
    """ The primary MFilesFS instance

    The primary filesystem instance that implements all the
    necessary filesystem calls. Mostly this class acts as a
    proxy between FUSE and the internal object oriented
    filesysstem model.

    For debug purposes this class doesn't currently derive
    from the Fuse class and must be wrapped inside the
    LoggingFS debug filesystem.
    """

    def __init__(self, *args, **kw):
        Fuse.__init__(self, *args, **kw)

        class wrap_file(FsFile):
            def __init__(s2, *a, **kw): FsFile.__init__(s2, self, *a, **kw)
        class wrap_dir(FsDir):
            def __init__(s2, *a, **kw): FsDir.__init__(s2, self, *a, **kw)

        self.file_class = wrap_file
        self.dir_class = wrap_dir

        # For now the rest session is built here. Consider refactoring this
        # into fsinit call at some point.
        mfwaroot = rest.Resource('http://dev.jubjubnest.net/mfwa/')
        mfwaroot.init_cookies()
        self.rest = mfwaroot.resource('rest')
        self.rest.put({'Username':'test', 'Password':'test'}, 'session')
        self.rest.put('{DBA22602-C28F-4E52-8578-9537904DDCE9}', 'session/vault')

        self.vault = vault.Vault(self.rest)

    def fsinit(self):
        logging.info('Filessystem initialized')
        self.bus = bus.CommBus(message_cb=lambda cmd,arg : self._message(cmd, arg))

    def fsdestroy(self):
        logging.info('Filesystem destroyed')


    def getattr(self, path):
        """ Get attributes for the filesystem object """
        path = fsdecode(path)

        logging.debug('getattr: %s' % path)
        entry = self._get_fsentry(path)
        if entry is not None:
            return entry
        else:
            return -errno.ENOENT

    def truncate(self, path, length):
        logging.info("Truncate: %s, %s" % (path, length))
        return 0

    def mknod(self, path, mode, dev):
        return -errno.EACCES
        pass

    def getxattr(self, path, name, unknown):
        logging.warn(path)
        logging.warn(name)
        logging.warn(unknown)
        return None

    def statfs(self):
        s = fuse.StatVfs()

        # Think this is close to M-Files' maxmimum for single line text fields such as
        # the name. Could be 100 though.
        s.f_namemax = 256

        s.f_flag = 0

        # Hopefully these won't matter as these aren't really based
        # on anything.

        # Free space and free space for non-super user
        s.f_free = 1024*1024
        s.f_favail = 1024*1024

        # Total and free file nodes
        s.f_files = 1024
        s.f_ffree = 1024

        # Preferrd and fundamental block size.
        s.f_bsize = 4096
        s.f_frsize = 4096

        return s

    def _get_fsentry(self, path):
        """ Retrieves the filesystem entry for the given path.
        
        Returns None if there is no such entry.
        """
        if path == "/.mfilesfs":
            logging.info("Returning hidden bus")
            return self.bus
        elif path.startswith("/.mfilesfs/"):
            return self.bus.channel(path[len("/.mfilesfs/"):])

        return self.vault.get_child(path)

    def _message(self, cmd, arg):
        c = cmds.get_cmd(cmd)
        return c.execute(self.vault, arg)

    def __getattr__(self, name):
        if name in ['open', 'create', 'opendir']: return Fuse.__getattr__(self, name)
        if name in dir(FsFile): return Fuse.__getattr__(self, name)
        if name in dir(FsDir): return Fuse.__getattr__(self, name)

        logging.error("Retrieving unimplemented member. %s" % name)

        return lambda *args, **kw: self._call(name, args, kw)

    def _call(self, name, *args, **kw):
        logging.warn('unimplemented method \'%s\' called' % name)
        pprint.pprint(args)
        return -errno.ENOSYS

if __name__ == '__main__':
        fuse.fuse_python_api = (0,2)
        #fs = LoggingFS(MFFS())
        fs = MFFS()
        #fs.flags = 0
        fs.multithreaded = 0
        fs.parse(errex=1)
        fs.main()
